#!/usr/bin/python
# Copyright 2010 Graphical Timeline authors (see AUTHORS file)
#
# This file is part of Graphical Timeline.
#
# Graphical Timeline is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Graphical TImeline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Graphical Timeline.  If not, see <http://www.gnu.org/licenses/>.

import time
import sys
import os
from cgi import escape

rowHeight = 20
branchWidth = 15 # Space reserved for a branch
xOffset = 10 # Horizontal offset/spacing
yOffset = 8 # Vertical offset 


def createSVG(changes, dposx, dposy, branchColors, onlyTimeline=True):
   # width is only required for alternating background...

   mysvgstuff = ''
   width = getWidth(dposx)
   height = getHeight(dposy)

   if not onlyTimeline:
      # Change width, so the changeset info will fit
      width = 1200
      # Draw background:
      mysvgstuff += '<g fill="#dddddd">\n'
      for y1 in range(len(changes)):
         if y1 % 2 is 1:
            mysvgstuff += ' <rect x="2" y="%d" width="%d" height="20" />\n' % (y1 * rowHeight, width)
      mysvgstuff += '</g>\n'

   # Draw the interconnect with smooth lines:
   mysvgstuff += '<g fill="none">\n'
   for ctx in changes:
      x1 = dposx[ctx.rev] * branchWidth + xOffset
      y1 = dposy[ctx.rev] * rowHeight + yOffset
      for p_rev in ctx.parents:
         if isStart(p_rev):
            continue
         # Find parent changeset
         p = filter(lambda c: c.rev == p_rev, changes)[0]
         x2 = dposx[p_rev] * branchWidth + xOffset
         y2 = dposy[p_rev] * rowHeight + yOffset

         color = branchColors[p.branch]

         mysvgstuff += ' <path d="'
         if x1 != x2:
            # Line goes from 1 branch to another

            # Determine space till next commit in branch
            delta1 = 0
            start = changes.index(ctx)
            for h in changes[start + 1:]:
               delta1 += rowHeight
               if h.branch is ctx.branch:
                  break
            delta2 = 0
            start = changes.index(p)
            temp = changes[:start]
            temp.reverse()
            for h in temp:
               delta2 += rowHeight
               if h.branch is p.branch:
                  break
            deltaY = y2 - y1
            delta1 = min(deltaY, delta1)
            delta2 = min(deltaY, delta2)
            x_cp1 = x1
            y_cp1 = y1 + delta1
            x_cp2 = x2
            y_cp2 = y2 - delta2

            movedTop = False
            movedLower = False
            # Cubic spline with two control points
            if delta2 > rowHeight:
               movedLower = True
               # Move lower coordinate as high as possible
               y2 -= (delta2 - rowHeight - 5)
               y_cp1 = y1 + rowHeight + 5
               y_cp2 = y2 - rowHeight - 5
            elif delta1 > rowHeight:
               # Move top coordinate as low as possible
               movedTop = True
               mysvgstuff += 'M%d,%d v%d' % (x1, y1, delta1 - rowHeight - 5)
               y1 += delta1 - rowHeight - 5
               y_cp1 = y1 + rowHeight + 5
               y_cp2 = y2 - rowHeight - 5
            if not movedTop:
               mysvgstuff += 'M%d,%d' % (x1, y1)
            mysvgstuff += ' C%d,%d %d,%d %d,%d' % (x_cp1, y_cp1, x_cp2, y_cp2, x2, y2)
            if movedLower:
               mysvgstuff += ' v%d' % (delta2 - rowHeight - 5)
         else:
            # Simple vertical line
            mysvgstuff += 'M%d,%d v%d' % (x1, y1, y2 - y1)
         mysvgstuff += '" stroke="%s" stroke-width="2" />\n' % (color)
   mysvgstuff += '</g>\n'

   # Points in time...
   mysvgstuff += '<g fill="gray" stroke="black">\n'
   for ctx in changes:
      mysvgstuff += ' <circle cx="%d" cy="%d" r="5" />\n' % (dposx[ctx.rev] * branchWidth + xOffset, dposy[ctx.rev] * rowHeight + yOffset)
   mysvgstuff += '</g>\n'

   # ... and t xt?
   if not onlyTimeline:
      xText = max(dposx.values()) * branchWidth + xOffset * 2
      fontSize = 16
      mysvgstuff += '<g font-family="Verdana" font-size="%d" fill="blue">\n' % (fontSize)
      for ctx in changes:
         info = ctx.branch + ' ' + escape(ctx.author, True) + ' ' + ctx.date.ctime()
         mysvgstuff += ' <text x="%d" y="%d">%s</text>\n' % (xText, dposy[ctx.rev] * rowHeight + yOffset + fontSize / 2, info)
      mysvgstuff += '</g>\n'

   svgfile = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' \
     '<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d" >' \
     '<g>%s</g>\n</svg>' % (width, height, width, height, mysvgstuff)

   return svgfile

def getWidth(dposx):
   return max(dposx.values()) * branchWidth + 2 * xOffset

def getHeight(dposy):
   return (max(dposy.values()) + 1) * rowHeight

def getTableInfo(changes):
   """ Returns the table information which can be used to create a custom (HTML) table next to the timeline"""
   tableInfo = []
   for ctx in changes:
      tableInfo.append({'hex': ctx.rev, 'user': ctx.author,
                        'date': ctx.date.strftime("%d-%m-%Y"),
                        'desc': ctx.message})
   return tableInfo

def isStart(rev):
   """ Returns True if the given changeset or revision number (string) is the first changeset of this repository """
   if hasattr(rev, "rev"):
      # rev has a 'rev' attribute, so we assume it is a MercurialChangeset
      # and get the revision number string (which is stored in the 'rev' attribute)
      rev =  rev.rev
   return rev == '0000000000000000000000000000000000000000'

def buildInfo(repo, selectedBranch):
   # Recursively select all changesets from the head till beginning of the selected branch

   # CSS color  red        blue       fuchsia    lime      aqua,      yellow      black     sienna (brown)
   colorMap = ['#FF0000', '#0000FF', '#FF00FF', '#00FF00', '#00FFFF', '#FFFF00', '#000000', '#A0522D']
   branchColors = {}
   nextColor = 0;
   branchechanges = {}

   def addChange(rev):
      # Add changes recursively 
      if isStart(rev) or rev in branchechanges.keys():
         return
      ctx = repo.get_changeset(rev)
      branchechanges[rev] = ctx
      for p_rev in ctx.parents:
         addChange(p_rev)
   branchTip = repo.repo[repo.repo.branchtags()[selectedBranch]].hex()
   addChange(branchTip)

   # Put the changes in array (instead of dictionary)
   changes = [branchechanges[hx] for hx in branchechanges]
   # and sort by date
   changes.sort(key=lambda i: i.date, reverse=True)

   # Check if parent is above a child, if not swap them:
   for ctx in changes:
      for p_rev in ctx.parents:
         if isStart(p_rev):
            continue
         p = filter(lambda c: c.rev == p_rev, changes)[0]
         i = changes.index(ctx)
         ip = changes.index(p)
         if i > ip:
              changes[i], changes[ip] = (changes[ip], changes[i])

   # Calculate the positions! (Cool stuff)
   # Start with list of active ctx containing only
   # the tip:
   active_ctx = [changes[0].rev]

   # Calculate coordinates for each commit indicator (circle)
   dposx = {} # In branch numbers
   dposy = {}
   ypos = 0
   for ctx in changes:
      key = ctx.rev
      if not ctx.branch in branchColors:
         branchColors[ctx.branch] = colorMap[nextColor]
         nextColor += 1
         if nextColor == len(colorMap):
            nextColor = 0
         
      dposx[key] = active_ctx.index(key) # The first occurence of key in the list
      dposy[key] = ypos
      ypos += 1
      # Adjust active ctx list:
      # First: remove children:
      #for c in ctx.children():
      #         k = c.hex()
      #         if k in active_ctx:
      #            active_ctx.remove(k)
      # Remove the node itself (all refs):
      while key in active_ctx:
         active_ctx.remove(key)
      # Next, add all parents:
      for p_rev in ctx.parents:
         active_ctx.insert(dposx[key], p_rev)

   return changes, dposx, dposy, branchColors
